PostgreSQL 행 수준 보안(RLS)으로 강력한 다중 테넌트 데이터 격리 달성
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
현대 소프트웨어 개발, 특히 SaaS 및 클라우드 기반 시스템의 세계에서 다중 테넌시는 핵심적인 아키텍처 패턴이 되었습니다. 단일 애플리케이션 인스턴스로 여러 고객(테넌트)을 지원할 수 있어 상당한 비용 절감과 관리 간소화를 가져옵니다. 그러나 이러한 효율성에는 중요한 과제가 따릅니다. 바로 테넌트 간의 절대적인 데이터 격리를 보장하는 것입니다. 이 격리의 침해는 개인 정보 침해, 보안 사고 및 심각한 평판 손상을 초래할 수 있습니다. 전통적으로 개발자들은 인증된 테넌트를 기반으로 데이터를 필터링하기 위해 애플리케이션 수준 로직에 크게 의존해 왔습니다. 어느 정도 효과적이기는 하지만, 이 접근 방식은 애플리케이션에 부담을 전적으로 싣게 되어 복잡성을 증가시키고 오류 가능성을 높이며 잠재적인 단일 실패 지점이 될 수 있습니다. 이 글은 PostgreSQL의 행 수준 보안(RLS)이 다중 테넌트 데이터 격리를 근본적으로 해결하기 위한 강력한 데이터베이스 네이티브 메커니즘을 제공하여 더욱 강력하고 안전한 솔루션을 제공하는 방법을 자세히 살펴봅니다.
기본 이해
RLS에 대해 자세히 알아보기 전에 몇 가지 핵심 개념을 명확하게 이해해 두겠습니다.
- 다중 테넌시: 단일 소프트웨어 애플리케이션 인스턴스가 여러 테넌트(고객 또는 그룹)를 지원하는 아키텍처입니다. 각 테넌트의 데이터는 다른 테넌트로부터 격리되지만, 모든 테넌트는 동일한 애플리케이션 인스턴스와 데이터베이스 스키마를 공유합니다.
 - 데이터 격리: 한 테넌트에 속한 데이터가 다른 테넌트가 액세스할 수 없도록 하고 보이지 않도록 보장하는 원칙입니다. 이는 보안 및 개인 정보 보호에 매우 중요합니다.
 - 행 수준 보안(RLS): 쿼리를 실행하는 사용자의 특성에 따라 개별 데이터 행에 대한 액세스를 제한하는 데이터베이스 기능으로, 전체 테이블이 아닌 개별 행에 대한 액세스를 제한합니다. 이 세분화된 제어는 데이터베이스 시스템 자체에서 강제됩니다.
 - 정책: RLS의 맥락에서 정책은 사용자가 액세스하거나 수정할 수 있는 행을 결정하는 테이블에 정의된 규칙 집합입니다. 정책은 
SELECT,INSERT,UPDATE,DELETE작업에 적용될 수 있습니다. 
PostgreSQL 행 수준 보안의 힘
PostgreSQL의 RLS는 정책을 테이블에 직접 연결하여 작동합니다. 이러한 정책은 액세스하거나 수정하려는 각 행에 대해 조건을 평가합니다. 조건이 참으로 평가되면 작업이 허용되고, 그렇지 않으면 거부됩니다. 이 적용은 쿼리 결과가 애플리케이션으로 반환되기 전에 발생하여 우회할 수 없는 보안 계층을 제공합니다.
다중 테넌트 격리를 위한 핵심 아이디어는 관련 테이블에 존재하는 tenant_id 열을 기반으로 행을 필터링하는 것입니다. 현재 테넌트의 ID를 RLS 정책에 통합함으로써 데이터베이스 자체는 해당 특정 테넌트에 속하는 행만 보이고 수정될 수 있도록 보장합니다.
작동 방식: 단계별 구현
실제 예제를 통해 설명해 보겠습니다. 각 제품이 특정 테넌트에 속하는 다중 테넌트 products 테이블을 상상해 보세요.
먼저, 데이터베이스가 어떤 테넌트가 현재 활성 상태인지 알 수 있는 방법이 필요합니다. PostgreSQL의 SET SESSION AUTHORIZATION 또는 다중 테넌시에 더 일반적으로 사용되는 SET LOCAL 변수가 이를 위해 완벽합니다. app.current_tenant_id라는 사용자 지정 세션 변수를 사용합니다.
-- 1. tenant_id를 포함하는 `products` 테이블 생성 CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, tenant_id INT NOT NULL ); -- 여러 테넌트에 대한 샘플 데이터 삽입 INSERT INTO products (name, price, tenant_id) VALUES ('Laptop A', 1200.00, 1), ('Mouse B', 25.00, 1), ('Keyboard C', 75.00, 2), ('Monitor D', 300.00, 1), ('Webcam E', 50.00, 2); -- 2. 테이블에서 행 수준 보안 활성화 ALTER TABLE products ENABLE ROW LEVEL SECURITY; -- 3. tenant_id를 기반으로 액세스 제한 정책 생성 -- 이 정책은 사용자가 자신의 current_tenant_id에 속하는 제품만 보고 수정할 수 있도록 보장합니다. CREATE POLICY tenant_isolation_policy ON products FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::int); -- 모든 새 제품이 올바르게 태그되도록 삽입을 위한 정책을 원할 수도 있습니다. -- 또는 "FOR ALL"이 사용되는 경우 위의 "USING" 절은 삽입에도 적용됩니다. -- INSERT, UPDATE, DELETE에 대한 더 엄격한 제어 또는 다른 로직의 경우 별도의 정책을 생성할 수 있습니다: -- CREATE POLICY insert_tenant_policy ON products FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::int); -- CREATE POLICY update_tenant_policy ON products FOR UPDATE USING (tenant_id = current_setting('app.current_tenant_id')::int) WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::int);
이제 이것을 실행해 보겠습니다. 애플리케이션이 데이터베이스에 연결하고 테넌트 1의 일부로 사용자를 인증하면, 데이터 쿼리 전에 다음 명령을 실행합니다.
SET app.current_tenant_id = '1';
그런 다음 애플리케이션에서 products 테이블에 대한 후속 쿼리는 자동으로 RLS에 의해 필터링됩니다.
-- 테넌트 1을 위한 애플리케이션 쿼리 SELECT * FROM products;
app.current_tenant_id = '1'에 대한 예상 출력:
| id | name | price | tenant_id | 
|---|---|---|---|
| 1 | Laptop A | 1200.00 | 1 | 
| 2 | Mouse B | 25.00 | 1 | 
| 4 | Monitor D | 300.00 | 1 | 
애플리케이션이 테넌트 2의 사용자를 인증한 후 컨텍스트를 테넌트 2로 전환하면:
SET app.current_tenant_id = '2'; -- 테넌트 2를 위한 애플리케이션 쿼리 SELECT * FROM products;
app.current_tenant_id = '2'에 대한 예상 출력:
| id | name | price | tenant_id | 
|---|---|---|---|
| 3 | Keyboard C | 75.00 | 2 | 
| 5 | Webcam E | 50.00 | 2 | 
SELECT 쿼리 자체는 일반적이라는 점에 유의하십시오. 필터링은 전적으로 데이터베이스로 위임되고 강제되며, 모든 애플리케이션 쿼리에서 WHERE tenant_id = <current_tenant_id> 절을 작성할 필요가 없습니다.
견고성과 장점
- 절대적인 데이터 격리: RLS는 최종 게이트키퍼 역할을 합니다. 애플리케이션 코드에 
WHERE tenant_id = X를 잊는 버그가 있더라도 데이터베이스는 여하튼 정책을 적용하여 데이터 유출을 방지합니다. - 애플리케이션 복잡성 감소: 개발자는 테넌트 필터링을 위한 상용구 코드를 적게 작성하여 비즈니스 로직에 집중할 수 있습니다.
 - 강화된 보안: 보안 문제를 데이터베이스 계층으로 푸시함으로써 SQL 인젝션 또는 기타 취약점을 통해 테넌트 격리를 우회하려는 공격자가 더 어려워집니다.
 - 중앙 집중식 제어: 보안 정책은 데이터베이스 수준에서 정의 및 관리되어 데이터와 상호 작용하는 모든 애플리케이션 부분 및 마이크로서비스 간에 일관성을 보장합니다.
 - 성능: PostgreSQL의 쿼리 플래너는 RLS 정책을 인식하고 쿼리 실행을 최적화하여 종종 
tenant_id에서 효율적인 인덱스 사용을 가져옵니다. 
고급 고려 사항
- RLS 우회 (슈퍼유저/관리자용): PostgreSQL 슈퍼유저 또는 
BYPASSRLS권한이 있는 역할은 정책을 우회할 수 있습니다. 이는 유지 보수, 백업 및 관리 작업에 중요하지만 극도의 주의를 기울여 사용해야 합니다. - 명령별 정책: RLS를 사용하면 
SELECT,INSERT,UPDATE,DELETE작업에 대해 별도의 정책을 정의하여 데이터 조작에 대한 세분화된 제어를 제공할 수 있습니다.WITH CHECK절은 새 행이나 수정된 행이 여전히 정책을 준수하도록 (예: 사용자가 다른 테넌트의 행을 삽입할 수 없음)INSERT및UPDATE정책에 특히 유용합니다. - 복잡한 테넌트 계층 구조: RLS는 테넌트가 부모-자식 관계 또는 공유 데이터를 가질 수 있는 보다 복잡한 시나리오를 처리할 수 있습니다. 정책은 복잡한 액세스 규칙을 구현하기 위해 함수 또는 하위 쿼리를 통합할 수 있습니다.
 
결론
PostgreSQL의 행 수준 보안은 다중 테넌트 데이터 격리를 위한 우아하고 강력한 솔루션을 제공합니다. 테넌트 수준 데이터 필터링의 부담을 애플리케이션 계층에서 데이터베이스 코어로 이동함으로써 RLS는 보안을 대폭 향상시키고 애플리케이션 복잡성을 줄이며 확고한 손으로 데이터 경계를 적용합니다. PostgreSQL에서 구축된 다중 테넌트 애플리케이션의 경우 RLS를 채택하는 것은 단순히 모범 사례일 뿐만 아니라 진정으로 안전하고 유지 가능한 아키텍처로 향하는 근본적인 단계입니다. 이는 데이터베이스가 테넌트 데이터 분리의 궁극적인 수호자가 되도록 합니다.